﻿#region Copyright (c) 2013-04, Olaf Kober <amarok.projects@gmail.com>
//================================================================================
//	Permission is hereby granted, free of charge, to any person obtaining a copy
//	of this software and associated documentation files (the "Software"), to deal
//	in the Software without restriction, including without limitation the rights
//	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//	copies of the Software, and to permit persons to whom the Software is
//	furnished to do so, subject to the following conditions:
//
//	The above copyright notice and this permission notice shall be included in
//	all copies or substantial portions of the Software.
//
//	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//	THE SOFTWARE.
//================================================================================
#endregion

using System;
using System.Diagnostics;
using System.Globalization;
using System.Threading.Tasks;
using Amarok.Agents.Resources;


namespace Amarok.Agents
{
	/// <summary>
	/// This type represents the base class for agent implementations. Derived types must implement an agent-specific
	/// constructor that accepts agent environment and agent options. The agent instance is fully operational directly
	/// after construction and can receive and send messages.
	/// 
	/// Inside the agent various overloaded methods can be used to post and publish messages. Posting messages will
	/// send them to the agent instance itself. This can be used to implement workflows or state machines. Publishing 
	/// messages will send them to the outside world, meaning to subscribed agents and/or to a connected message bus, 
	/// which further forwards the messages to connected agents.
	/// </summary>
	public abstract partial class Agent : CompletableObject,
		IAgent,
		IAgentProxy
	{
		// static data
		private static IAgentFactory sFactory = new DefaultAgentFactory();

		// data
		private readonly IDispatcher mDispatcher;
		private readonly IPublisher mPublisher;
		private readonly IAdvertiser mAdvertiser;
		private readonly AgentEnvironment mEnvironment;
		private readonly AgentOptions mOptions;

		// state
		private Object mLabel;


		#region ++ Public Static Interface ++

		/// <summary>
		/// Gets a reference to the agent factory.
		/// </summary>
		public static IAgentFactory Factory
		{
			get
			{
				return sFactory;
			}
		}


		/// <summary>
		/// Replaces the default agent factory by the factory supplied to the method.
		/// </summary>
		/// 
		/// <param name="factory">
		/// The new agent factory to use.</param>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		public static void SetFactory(IAgentFactory factory)
		{
			Verify.NotNull(factory, "factory");
			sFactory = factory;
		}

		#endregion

		#region ++ ICompletable Interface ++

		/// <summary>
		/// Gets a Task instance that represents the completion state of the object. This task instance is 
		/// transitioned into the final RunToCompletion state when the object has finally completed.
		/// </summary>
		Task ICompletable.Completion
		{
			get
			{
				return base.Completion;
			}
		}


		/// <summary>
		/// Initiates the asynchronous completion of the object. After calling this method the object will complete 
		/// and its <see cref="ICompletable.Completion"/> task will enter a final state after it has processed or 
		/// discarded all previously available messages.
		/// </summary>
		/// 
		/// <param name="discardAvailableMessages">
		/// True to discard all already available messages. False to process all already available messages to 
		/// completion. Defaults to false.</param>
		void ICompletable.Complete(Boolean discardAvailableMessages)
		{
			base.CompleteObject(discardAvailableMessages);
		}

		#endregion

		#region ++ IDispatcher Interface ++

		/// <summary>
		/// Posts the message to the dispatcher.
		/// 
		/// The method stores the supplied message in the dispatcher's input message queues of interested consumers and 
		/// returns immediately, meaning it only schedules the message for further processing, but the actual processing 
		/// is done later in other threads.
		/// 
		/// Messages that the dispatcher is unable to handle, i.e. because no corresponding message handler exists, 
		/// will be dropped silently.
		/// 
		/// If there is no interested consumer at the time of posting, then the message will be dropped silently.
		/// 
		/// Messages that are posted after completing the dispatcher will be dropped silently.
		/// </summary>
		/// 
		/// <param name="message">
		/// The message that should be posted.</param>
		/// 
		/// <returns>
		/// A boolean value indicating whether the message being posted has been scheduled for further processing, or 
		/// False, if the message has been filter, the dispatcher is completing or already completed, or no matching 
		/// message handler exists.
		/// </returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		Boolean IDispatcher.Post(Message message)
		{
			return mDispatcher.Post(message);
		}

		#endregion

		#region ++ IAdvertiser Interface ++

		/// <summary>
		/// Subscribes the supplied agent on the publisher. All messages published that match or derive 
		/// from the specified message type and that pass the optional message filter will be forwarded 
		/// to the subscriber.
		/// </summary>
		/// 
		/// <param name="messageType">
		/// The base type of messages for which to subscribe. This type must be assignable to <see cref="Message"/>.</param>
		/// <param name="subscriber">
		/// An agent reference to the agent subscribing for messages.</param>
		/// <param name="messageFilter">
		/// A callback used as content-based filter. Optional, defaults to null.</param>
		/// 
		/// <returns>
		/// An object representing the subscription that can be used to unsubscribe from the publisher, 
		/// or a null value when the advertiser has already be completed.</returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// A value was passed to a method that did not accept it as a valid argument, because 
		/// the value representing a type is not assignable to the specified target type.</exception>
		ICompletable IAdvertiser.Subscribe(
			Type messageType,
			AgentReference subscriber,
			Predicate<Message> messageFilter)
		{
			return mAdvertiser.Subscribe(messageType, subscriber, messageFilter);
		}

		/// <summary>
		/// Subscribes the supplied callback on the publisher. All messages published that match or derive 
		/// from the specified message type and that pass the optional message filter will be forwarded 
		/// to the subscriber.
		/// </summary>
		/// 
		/// <param name="messageType">
		/// The base type of messages for which to subscribe. This type must be assignable to <see cref="Message"/>.</param>
		/// <param name="subscriber">
		/// A callback subscribing for messages.</param>
		/// <param name="messageFilter">
		/// A callback used as content-based filter. Optional, defaults to null.</param>
		/// 
		/// <returns>
		/// An object representing the subscription that can be used to unsubscribe from the publisher, 
		/// or a null value when the advertiser has already be completed.
		/// </returns>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="ArgumentException">
		/// A value was passed to a method that did not accept it as a valid argument, because 
		/// the value representing a type is not assignable to the specified target type.</exception>
		ICompletable IAdvertiser.Subscribe(
			Type messageType,
			Action<Message> subscriber,
			Predicate<Message> messageFilter)
		{
			return mAdvertiser.Subscribe(messageType, subscriber, messageFilter);
		}

		#endregion

		#region ++ IPublisher Interface ++

		/// <summary>
		/// Publishes the supplied message and forwards it to interested consumers.
		/// 
		/// The method stores the supplied message in the dispatcher's input message queues of interested consumers and 
		/// returns immediately, meaning it only schedules the message for further processing, but the actual processing 
		/// is done later in other threads.
		/// 
		/// If there is no interested consumer at the time of publishing, then the message will be dropped silently.
		/// 
		/// Messages that are published after completing the publisher will be dropped silently.
		/// </summary>
		/// 
		/// <param name="message">
		/// The message that should be published.</param>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		void IPublisher.Publish(Message message)
		{
			mPublisher.Publish(message);
		}

		#endregion

		#region ## Protected Interface (Label) ##

		/// <summary>
		/// Gets or sets the label of this agent. When set, only messages that have no label or messages whose label 
		/// is equal to the agent's label will be processed. All other messages will be discarded by the agent.
		/// 
		/// The label can be set only once by assigning a non-null object to this property. Trying to overwrite
		/// the label, after it has been set, will result in an exception. You have to reset it first by calling 
		/// <see cref="ResetLabel"/> before you can set the label once again.
		/// </summary>
		/// 
		/// <value>
		/// A null reference when no label is assigned to this agent; otherwise the object representing the label.
		/// </value>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		/// <exception cref="InvalidOperationException">
		/// The label of an agent can be set only once. You have to reset it first before you can set the label 
		/// once again.</exception>
		protected Object Label
		{
			get
			{
				return mLabel;
			}
			set
			{
				Verify.NotNull(value, "Label");

				if (this.HasLabel)
					throw new InvalidOperationException(AgentResources.AgentLabelCannotBeSetTwice);

				mLabel = value;
			}
		}

		/// <summary>
		/// Gets a boolean value indicating whether this agent has a label assigned to it.
		/// </summary>
		protected Boolean HasLabel
		{
			get
			{
				return mLabel != null;
			}
		}


		/// <summary>
		/// Resets the label of this agent to a null reference. After calling this method, it is possible 
		/// to set the label once again.
		/// </summary>
		protected void ResetLabel()
		{
			mLabel = null;
		}

		#endregion

		#region ## Protected Interface ##

		/// <summary>
		/// Gets a reference to the agent's environment. The agent environment is used to give agents access to 
		/// the services of the runtime environment. One agent environment instance is shared between multiple 
		/// agents to provide the same services to all agents.
		/// </summary>
		protected AgentEnvironment Environment
		{
			get
			{
				return mEnvironment;
			}
		}

		/// <summary>
		/// Gets a reference to the agent's options. Agent options can be used to provide agent instances with 
		/// individual configurations and settings.
		/// </summary>
		protected AgentOptions Options
		{
			get
			{
				return mOptions;
			}
		}


		/// <summary>
		/// Initializes a new instance.
		/// </summary>
		/// 
		/// <param name="environment">
		/// The agent environment for this agent.</param>
		/// <param name="options">
		/// The agent options for this agent.</param>
		/// 
		/// <exception cref="ArgumentNullException">
		/// A null reference was passed to a method that did not accept it as a valid argument.</exception>
		protected Agent(AgentEnvironment environment, AgentOptions options)
		{
			Verify.NotNull(environment, "environment");
			Verify.NotNull(options, "options");

			// store references
			mEnvironment = environment;
			mOptions = options;

			// initialize publisher
			var publisherImpl = new DefaultPublisher();
			var publisherHook = new PublisherHookDecorator(this, publisherImpl, publisherImpl, publisherImpl);
			mPublisher = publisherHook;
			mAdvertiser = publisherHook;

			// initialize dispatcher
			var dispatcherImpl = new DefaultDispatcher(this, _HandleInitialize, _HandleFilter);
			var dispatcherHook = new DispatcherHookDecorator(this, dispatcherImpl, dispatcherImpl);
			mDispatcher = dispatcherHook;
		}


		/// <summary>
		/// Returns a new agent reference that refers to this agent.
		/// </summary>
		protected AgentReference ToAgentReference()
		{
			return AgentExtensions.ToAgentReference(this);
		}


		/// <summary>
		/// Returns a new agent reference that refers to the specified agent.
		/// </summary>
		/// 
		/// <param name="agent">
		/// The agent to which the agent reference should refer. Null references are allowed.</param>
		/// 
		/// <returns>
		/// An agent reference that refers to the supplied agent, or null if a null reference was supplied.</returns>
		public static implicit operator AgentReference(Agent agent)
		{
			return AgentExtensions.ToAgentReference(agent);
		}

		#endregion

		#region ## Overridable Methods ##

		/// <summary>
		/// An overrideable method that allows derived types to implement custom initialization logic. The 
		/// initialization is triggered with the first message that the agent receives, but only messages
		/// that actually will be handled by the agent will trigger the initialization.
		/// 
		/// This method is guaranteed to be called once. In addition, it is guaranteed that this method is called 
		/// and executed to completion before the first exclusive or concurrent message handler will run, but it 
		/// cannot be guaranteed that this method is not executed concurrently to independent handlers.
		/// </summary>
		protected virtual void OnInitialize()
		{
			// default implementation
		}

		/// <summary>
		/// An overridable method that allows derived types to implement custom message filtering. The filter 
		/// method is called for each incoming message. The default implementation permits the message if either
		/// the agent or the message have no label assigned. If both agent and message have a label assigned, then
		/// only messages with a label that equals the label of the agent are permitted.
		/// </summary>
		/// 
		/// <param name="message">
		/// The incoming message.</param>
		/// 
		/// <returns>
		/// True, if the message should be permitted; otherwise False to discard the message.</returns>
		protected virtual Boolean OnFilter(Message message)
		{
			if (this.HasLabel == false || message.HasLabel == false)
				return true;

			return this.Label.Equals(message.Label);
		}

		/// <summary>
		/// An overrideable method that allows derived types to implement custom cleanup logic. The cleanup
		/// method is called at last after the agent run to completion.
		/// 
		/// This method is guaranteed to be called once. No other message handlers will run or start.
		/// </summary>
		protected virtual void OnCleanup()
		{
			// default implementation
		}

		#endregion

		#region Implementation

		/// <summary>
		/// This overrideable method supports the agent infrastructure and is not intended to be used directly.
		/// </summary>
		protected override Task OnComplete(Boolean discardAvailableMessages)
		{
			var taskSource = new TaskCompletionSource();

			var task = new Task(() =>
				{
					// first, complete dispatcher so that no new messages will be processed
					((ICompletable)mDispatcher).Complete(discardAvailableMessages);
					((ICompletable)mDispatcher).Completion.ContinueWith(t1 =>
						{
							// then, complete the publisher to prevent any new messages published
							((ICompletable)mPublisher).Complete(discardAvailableMessages);
							((ICompletable)mPublisher).Completion.ContinueWith(t2 =>
								{
									// done
									taskSource.SignalCompletion();
								},
								TaskContinuationOptions.ExecuteSynchronously);
						},
						TaskContinuationOptions.ExecuteSynchronously);
				});

			task.Start(TaskScheduler.Default);

			return taskSource.Task;
		}

		/// <summary>
		/// This overrideable method supports the agent infrastructure and is not intended to be used directly.
		/// </summary>
		protected override void OnCompleted()
		{
			try
			{
				OnCleanup();
			}
			catch (Exception exception)
			{
				#region (tracing)
				{
					Trace.WriteLine(String.Format(CultureInfo.InvariantCulture,
						"*** WARNING: UNHANDLED EXCEPTION in Agent.OnCleanup():" +
						"\r\n  ObjectId: {0}" +
						"\r\n\r\n{1}",
						base.ObjectId,
						Helper.FormatException(exception, CultureInfo.InvariantCulture)
					));
				}
				#endregion
			}
		}

		private void _HandleInitialize()
		{
			try
			{
				OnInitialize();
			}
			catch (Exception exception)
			{
				#region (tracing)
				{
					Trace.WriteLine(String.Format(CultureInfo.InvariantCulture,
						"*** WARNING: UNHANDLED EXCEPTION in Agent.OnInitialize():" +
						"\r\n  ObjectId: {0}" +
						"\r\n\r\n{1}",
						base.ObjectId,
						Helper.FormatException(exception, CultureInfo.InvariantCulture)
					));
				}
				#endregion
			}
		}

		private Boolean _HandleFilter(Message message)
		{
			try
			{
				return OnFilter(message);
			}
			catch (Exception exception)
			{
				#region (tracing)
				{
					Trace.WriteLine(String.Format(CultureInfo.InvariantCulture,
						"*** WARNING: UNHANDLED EXCEPTION in Agent.OnFilter():" +
						"\r\n  ObjectId: {0}" +
						"\r\n\r\n{1}",
						base.ObjectId,
						Helper.FormatException(exception, CultureInfo.InvariantCulture)
					));
				}
				#endregion

				return false;
			}
		}

		#endregion

		#region Implementation (Hooks)

		private sealed class PublisherHookDecorator :
			IPublisher,
			IAdvertiser,
			ICompletable
		{
			private readonly IAgent mAgent;
			private readonly IPublisher mInnerPublisher;
			private readonly IAdvertiser mInnerAdvertiser;
			private readonly ICompletable mInnerCompletable;


			public PublisherHookDecorator(IAgent agent, IPublisher innerPublisher, IAdvertiser innerAdvertiser, ICompletable innerCompletable)
			{
				Verify.NotNull(agent, "agent");
				Verify.NotNull(innerPublisher, "innerPublisher");
				Verify.NotNull(innerAdvertiser, "innerAdvertiser");
				Verify.NotNull(innerCompletable, "innerCompletable");

				mAgent = agent;
				mInnerPublisher = innerPublisher;
				mInnerAdvertiser = innerAdvertiser;
				mInnerCompletable = innerCompletable;
			}

			void IPublisher.Publish(Message message)
			{
				// call inner implementation
				mInnerPublisher.Publish(message);

				// call hook
				AgentHooks.RaiseMessagePublished(mAgent, message);
			}

			ICompletable IAdvertiser.Subscribe(Type messageType, AgentReference subscriber, Predicate<Message> messageFilter)
			{
				// call inner implementation
				return mInnerAdvertiser.Subscribe(messageType, subscriber, messageFilter);
			}

			ICompletable IAdvertiser.Subscribe(Type messageType, Action<Message> subscriber, Predicate<Message> messageFilter)
			{
				// call inner implementation
				return mInnerAdvertiser.Subscribe(messageType, subscriber, messageFilter);
			}

			Task ICompletable.Completion
			{
				get
				{
					// call inner implementation
					return mInnerCompletable.Completion;
				}
			}

			void ICompletable.Complete(Boolean discardAvailableMessages)
			{
				// call inner implementation
				mInnerCompletable.Complete(discardAvailableMessages);
			}

			void IDisposable.Dispose()
			{
				// call inner implementation
				mInnerCompletable.Dispose();
			}
		}

		private sealed class DispatcherHookDecorator :
			IDispatcher,
			ICompletable
		{
			private readonly IAgent mAgent;
			private readonly IDispatcher mInnerDispatcher;
			private readonly ICompletable mInnerCompletable;


			public DispatcherHookDecorator(IAgent agent, IDispatcher innerDispatcher, ICompletable innerCompletable)
			{
				Verify.NotNull(agent, "agent");
				Verify.NotNull(innerDispatcher, "innerDispatcher");
				Verify.NotNull(innerCompletable, "innerCompletable");

				mAgent = agent;
				mInnerDispatcher = innerDispatcher;
				mInnerCompletable = innerCompletable;
			}

			Boolean IDispatcher.Post(Message message)
			{
				// call inner implementation
				var result = mInnerDispatcher.Post(message);

				// call hook
				if (result)
				{
					AgentHooks.RaiseMessagePosted(mAgent, message);
				}

				return result;
			}

			Task ICompletable.Completion
			{
				get
				{
					// call inner implementation
					return mInnerCompletable.Completion;
				}
			}

			void ICompletable.Complete(Boolean discardAvailableMessages)
			{
				// call inner implementation
				mInnerCompletable.Complete(discardAvailableMessages);
			}

			void IDisposable.Dispose()
			{
				// call inner implementation
				mInnerCompletable.Dispose();
			}
		}

		#endregion

	}
}
